import path from 'path'
import { fileURLToPath } from 'url'
import { Worker } from 'worker_threads'
import { asyncState, PromiseState } from '../state'
import { callbackMap } from '../transfer'
import { once } from '../utils'
import {
  MESSAGE_CALL,
  MESSAGE_CALLBACK,
  MESSAGE_CALLBACK_RESULT_ERROR,
  MESSAGE_CALLBACK_RESULT_SUCCESS,
  MESSAGE_CALL_ERROR,
  MESSAGE_CALL_SUCCESS,
  MESSAGE_EXIT,
  MESSAGE_EXITED,
  MESSAGE_READY
} from './types'

const _fileName = typeof __filename === 'string' ? __filename : fileURLToPath(import.meta.url)
const WORKER_ENTRY = path.join(path.dirname(_fileName), 'worker-entry.mjs')
const {
  getPrototypeOf: ObjectGetPrototypeOf,
  setPrototypeOf: ObjectSetPrototypeOf,
  prototype: ObjectPrototype
} = Object

type InvokePromiseState = PromiseState<any>

export class FastWorkerItemStatusError extends Error {}

const argv = process.argv.slice(2)
const execArgv = [...process.execArgv]
if (!execArgv.some(arg => arg.startsWith('--experimental-specifier-resolution'))) {
  execArgv.push('--experimental-specifier-resolution=node')
}
if (!execArgv.includes('--experimental-import-meta-resolve')) {
  execArgv.unshift('--experimental-import-meta-resolve')
}
if (!execArgv.includes('--experimental-modules')) {
  execArgv.unshift('--experimental-modules')
}

export class FastWorkerItem extends Worker {
  private _ready
  private _runnings
  private _closed
  private _callStates
  private _exitPromise
  private _terminate
  private _k

  constructor(env?: Record<keyof any, any>) {
    super(WORKER_ENTRY, {
      env: env ? { ...process.env, ...env } : undefined,
      argv,
      execArgv
    })

    this._runnings = 0
    this._closed = false
    this._callStates = new Map<any, InvokePromiseState>()
    this._exitPromise = asyncState()
    this._k = 0
    this._ready = asyncState()
    this._terminate = once(super.terminate.bind(this))
    this._ready.catch(() => this.terminate())

    let readyTimeoutTimer = setTimeout(() => {
      readyTimeoutTimer = undefined!
      this._ready.reject(new FastWorkerItemStatusError('Worker-thread load timeout.'))
    }, 0x7fff)

    this.on('exit', () => {
      this._closed = true
      this._ready.reject(new FastWorkerItemStatusError('Worker-thread is exited.'))
      this._exitPromise.resolve()
    })

    this.on('message', data => {
      switch (data[0]) {
        case MESSAGE_CALLBACK:
          {
            const [_, callKey, callbackId, args] = data
            const callback = callbackMap.get(callbackId)
            if (callback) {
              _postCallbackResult(this, callback, callKey, args).catch()
            } else {
              this.postMessage([
                MESSAGE_CALLBACK_RESULT_ERROR,
                callKey,
                new Error(
                  `Callback id '${callbackId} is notfound.\nIn the worker thread, the lifetime of the 'callback' function is limited to the execution of the function, and you may have called the 'callback' function outside of the execution of the function'`
                )
              ])
            }
          }
          break
        case MESSAGE_CALL_SUCCESS:
          {
            const [_, callKey, result] = data
            this._callStates.get(callKey)?.resolve(result)
          }
          break
        case MESSAGE_CALL_ERROR:
          {
            const [_, callKey, result] = data
            if (result && typeof result === 'object') {
              switch (ObjectGetPrototypeOf(result)) {
                case null:
                case ObjectPrototype:
                  ObjectSetPrototypeOf(result, Error.prototype)

                  break
              }
            }
            this._callStates.get(callKey)?.reject(result)
          }
          break
        case MESSAGE_EXITED:
          this._closed = true
          this._ready.reject(new FastWorkerItemStatusError('Worker-thread is exited.'))
          this._exitPromise.resolve()
          break
        case MESSAGE_READY:
          clearTimeout(readyTimeoutTimer)
          this._ready.resolve()
          break
      }
    })

    this._exitPromise.finally(() => {
      for (const _state of this._callStates.values()) {
        _state.reject(new FastWorkerItemStatusError('Worker-thread is exited.'))
      }
      this._callStates.clear()
    })
  }

  get ready() {
    return this._ready.fulfilled
  }

  get closed() {
    return this._closed
  }

  get runnings() {
    return this._runnings
  }

  whenReady(): Promise<void> {
    return this._ready
  }

  whenClosed(): Promise<void> {
    return this._exitPromise
  }

  invokeMethod(
    moduleId: string,
    method: string,
    noCallbackArgs: any[],
    callbackArgs: (string | number)[],
    timeout: number | null
  ) {
    return this._invoke(
      MESSAGE_CALL,
      [moduleId, method, noCallbackArgs, callbackArgs],
      timeout,
      () => new Error(`Invoke method '${method}' in module '${moduleId}' timeout.`)
    )
  }

  async terminate() {
    this.postMessage([MESSAGE_EXIT])
    await this._exitPromise
    return this._terminate()
  }

  private async _invoke(request: number, transferArgs: any[], timeout: number | null, timeoutError: () => Error) {
    if (this._closed) {
      throw new FastWorkerItemStatusError('Worker-thread is exited.')
    }

    if (!this.ready) {
      await this.whenReady()

      if (this._closed) {
        throw new FastWorkerItemStatusError('Worker-thread is exited.')
      }
    }

    const key = ++this._k
    const state: InvokePromiseState = asyncState<any>()
    this._callStates.set(key, state)

    let timer =
      timeout !== null && timeout >= 0
        ? setTimeout(() => {
            timer = undefined!
            state.reject(timeoutError())
          }, timeout)
        : undefined

    ++this._runnings
    this.postMessage([request, key, ...transferArgs])
    try {
      return await state
    } finally {
      --this._runnings
      if (timer !== undefined) {
        clearTimeout(timer)
      }
      this._callStates.delete(key)
    }
  }
}

async function _postCallbackResult(worker: FastWorkerItem, callback: Function, callKey: string, args: any[]) {
  try {
    const result = await callback(...args)
    worker.postMessage([MESSAGE_CALLBACK_RESULT_SUCCESS, callKey, result])
  } catch (error) {
    worker.postMessage([MESSAGE_CALLBACK_RESULT_ERROR, callKey, closed])
  }
}
